Interactive maps are a great medium for data visualization because they allow users to explore a dataset for themselves. Most interactive maps have functionality for a user to pan & zoom around a map, click on markers, and get more data from popup windows. Creating an interactive map also highlights the creator’s ability to conduct back-end data acquisition and processing, as well as to design an effective front-end user experience.
For this reason, adding an interactive map to your portfolio is a stellar way to showcase your ability to clients, make yourself stand out to potential employers, or share a personal project with the wider community.
We will walk through the entire workflow to produce an interactive map of London historical markers from data acquisition and processing, through map design and hosting online.
When exploring London city streets, you can immerse yourself in history by reading some of the thousands of historical markers connecting locations with notable events, buildings, and people throughout history. However, not everyone will have the chance to do this in person. An interactive map is an ideal medium for users to explore and learn more about the history of London from a database of historical markers.
Although we choose historical plaques in London, the general workflow here can be followed for any other dataset and/or location. We highly suggest you choose a location and dataset of interest to you -- data research and acquisition are fantastically useful skills! See the post on [Free GIS Data and Where to Find it](https://mapscaping.com/free-gis-data-and-where-to-find-it/) for some good tips.
If you find this tutorial helpful in making a map of your own please share it with us @TWITTER LINKEDIN.
A Python 3.8 environment with the following packages: Folium 0.12.1, GeoPandas 0.9.0, and Pandas 1.4.2. If you set up a geospatial Python environment following our tutorial here, you’re good to go.
We source London borough boundaries and historic plaque data from the following sources.
OpenPlaques is a crowdsourced database of historical markers. We use data from the UK only, but the database includes historic markers across the globe (consider aiming your map at your favorite city!) Using Pandas and GeoPandas, read the data into GeoDataFrames and view the top five entries.
import folium
from folium.plugins import MarkerCluster
import geopandas as gpd
import pandas as pd
First read the Plaques JSON data in with pandas. We can see that it contains latitude and longitude fields. We can constrict point geometries using "points_from_xy" to create GeodDataFrame.
There are also fields for the date it was placed, url to the info page, title, and inscription. All of those fields can be included in a popup window.
# Read in UK Historical Plaques data
plaques_df = pd.read_json("https://s3.eu-west-2.amazonaws.com/openplaques/open-plaques-United-Kingdom-2021-06-26.json").dropna().drop('updated_at', axis=1)
plaques = gpd.GeoDataFrame(
plaques_df, geometry=gpd.points_from_xy(plaques_df.longitude, plaques_df.latitude, crs=4326)
)
print(len(plaques_df.index))
plaques_df = None
plaques.head()
3370
| id | erected_at | latitude | longitude | inscription | is_current | uri | title | address | subjects | ... | geolocated? | photographed? | thumbnail_url | photos | organisations | language | area | people | see_also | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 43575 | 2015-02-21 | 51.64407 | -3.32798 | Sefydlwyd y blaid lafur yn sgil streic Rheilff... | True | https://openplaques.org/plaques/43575 | John Ewington blue plaque | Abercynon Railway Station | John Ewington | ... | True | True | https://farm5.staticflickr.com/4314/3586827949... | [{'uri': 'https://openplaques.org/photos/43682... | [{'name': 'Rhondda Cynon Taf Council', 'uri': ... | {'name': 'Welsh and English', 'alpha2': 'cy-en'} | {'name': 'Abercynon', 'uri': 'https://openplaq... | [{'uri': 'https://openplaques.org/people/19814... | [] | POINT (-3.32798 51.64407) |
| 113 | 3598 | 1975-01-01 | 57.14280 | -2.10666 | Archibald Simpson architect 1790-1847 a pionee... | True | https://openplaques.org/plaques/3598 | Archibald Simpson stone plaque | Bon-Accord Square | Archibald Simpson | ... | True | True | https://commons.wikimedia.org/wiki/Special:Fil... | [{'uri': 'https://openplaques.org/photos/58948... | [{'name': 'Aberdeen Civic Society', 'uri': 'ht... | {'name': 'English', 'alpha2': 'en'} | {'name': 'Aberdeen', 'uri': 'https://openplaqu... | [{'uri': 'https://openplaques.org/people/3066.... | [] | POINT (-2.10666 57.14280) |
| 114 | 3600 | 1957-11-01 | 57.14772 | -2.10140 | To the glory of God and in sacred memory of Ma... | True | https://openplaques.org/plaques/3600 | Mary Slessor stone plaque | The Pier, Former United Presbyterian Church (n... | Mary Slessor | ... | True | True | https://farm9.staticflickr.com/8372/8566321134... | [{'uri': 'https://openplaques.org/photos/10490... | [{'name': 'Rotary Club of Aberdeen', 'uri': 'h... | {'name': 'English', 'alpha2': 'en'} | {'name': 'Aberdeen', 'uri': 'https://openplaqu... | [{'uri': 'https://openplaques.org/people/4869.... | [{'uri': 'https://openplaques.org/plaques/5200... | POINT (-2.10140 57.14772) |
| 146 | 53629 | 2019-01-01 | 57.14959 | -2.09458 | In an earlier church on this site the Methodis... | True | https://openplaques.org/plaques/53629 | Methodist Society, Aberdeen black plaque | Queen Street | Methodist Society, Aberdeen | ... | True | True | https://farm66.staticflickr.com/65535/49926758... | [{'uri': 'https://openplaques.org/photos/60781... | [{'name': 'City of Aberden', 'uri': 'https://o... | {'name': 'English', 'alpha2': 'en'} | {'name': 'Aberdeen', 'uri': 'https://openplaqu... | [{'uri': 'https://openplaques.org/people/23627... | [] | POINT (-2.09458 57.14959) |
| 156 | 39770 | 2008-10-23 | 51.82135 | -3.01625 | This Tithe Barn was opened by His Royal Highne... | True | https://openplaques.org/plaques/39770 | Charles P A G Mountbatten-Windsor and Tithe Ba... | Monk Street | Charles P A G Mountbatten-Windsor and Tithe Ba... | ... | True | True | https://farm1.staticflickr.com/544/18406337484... | [{'uri': 'https://openplaques.org/photos/25509... | [] | {'name': 'Welsh', 'alpha2': 'cy'} | {'name': 'Abergavenny', 'uri': 'https://openpl... | [{'uri': 'https://openplaques.org/people/7572.... | [{'uri': 'https://openplaques.org/plaques/7897... | POINT (-3.01625 51.82135) |
5 rows × 22 columns
A kind soul has converted london bourough data to geojson and made it publicall available. We read the Boroughs data directly into a GeoDataFrame as multipolygon features.
Note the field for name and area
# Read in London boroughs data
boroughs = gpd.read_file("https://skgrange.github.io/www/data/london_boroughs.json")
boroughs.head()
| id | name | code | area_hectares | inner_statistical | geometry | |
|---|---|---|---|---|---|---|
| 0 | 1 | Kingston upon Thames | E09000021 | 3726.117 | False | MULTIPOLYGON (((-0.33068 51.32901, -0.33059 51... |
| 1 | 2 | Croydon | E09000008 | 8649.441 | False | MULTIPOLYGON (((-0.06402 51.31864, -0.06408 51... |
| 2 | 3 | Bromley | E09000006 | 15013.487 | False | MULTIPOLYGON (((0.01213 51.29960, 0.01196 51.2... |
| 3 | 4 | Hounslow | E09000018 | 5658.541 | False | MULTIPOLYGON (((-0.24456 51.48870, -0.24468 51... |
| 4 | 5 | Ealing | E09000009 | 5554.428 | False | MULTIPOLYGON (((-0.41183 51.53408, -0.41188 51... |
In this example, we will incorporate both a clickable point layer and a polygon choropleth layer. To connect our choropleth layer to the Plaques point features, we perform a spatial join to extract the number of historical Plaque points that are contained within each Borough polygon.
# Find the number of plaques in each borough
boroughs = boroughs.join(
gpd.sjoin(plaques, boroughs).groupby("index_right").size().rename("numPlaques"),
how="left",
)
Knowing how many markers are in each borough is a fairly useful metric, but we can do better. Say you are planning a trip and would like to see as many Plaques as possible in a single day, knowing the Plaque density would be a more useful metric.
The Boroughs GeoDataFrame contains an area field in hectares. We convert this is more intuitive square kilometers and calculate the Plaque density for each Borough polygon.
# Calculate the plaque density
boroughs['area_sqkm'] = boroughs['area_hectares'] / 100
boroughs['PlaqueDensity_sqkm'] = boroughs['numPlaques'] / boroughs['area_sqkm']
Now we have the data in the proper formats and are ready to plot on a map.
Initializing a map with Folium is as simple as calling the “Map” function with the coordinates of the map center.
map = folium.Map(location=[51.507351, -0.127758])
map
There are several parameters we can apply including defining the zoom_start and base map style. Folium uses OpenStreetMap as the base map by default, but we choose Stamen Toner because the style is cleaner and looks more historical. We also reduce the map height to make room for a title.
# Initialize map over London
map = folium.Map(location=[51.507351, -0.127758], tiles="Stamen Toner", zoom_start=10, height='90%', prefer_canvas=True)
# Add a title
title_html = '''
<h5 align="center"; margin-bottom=0px; padding-bottom=0px; style="font-size:20px"><b>Historical Plaques of London</b></h3>
<h5 align="center"; margin-top=0px; padding-top=0px; style="font-size:14px">Click on the blue circles for more info</h3>
'''
map.get_root().html.add_child(folium.Element(title_html))
map
There are 3370 markers in our historical Plaques dataset. Plotting each plaque as an individual marker would make the map hard to read, especially when zoomed out. We will utilize the MarkerCluster feature in Folium to aggregate nearby markers together when the map is zoomed out. After adding the features we will see that this makes the map look cleaner while still conveying the general location of historical Plaques.
The for loop below configures a popup, styles the Plaque marker, and adds the feature to the MarkerCluster layer.
# Create a marker for each plaque location. Format popup
marker_cluster = MarkerCluster().add_to(map)
for index, row in plaques.iterrows():
html = f"""<strong>Title:</strong> {row['title']}<br>
<br>
<strong>Inscription:</strong> {row['inscription']}<br>
<br>
<strong>Erected:</strong> {row['erected_at']}<br>
<br>
Find more info <a href={row['uri']} target="_blank">here</a><br>
"""
iframe = folium.IFrame(html,
width=200,
height=200)
popup = folium.Popup(iframe,
max_width=400)
folium.CircleMarker(location=[row["latitude"], row["longitude"]],
radius=10,
color="#3186cc",
fill=True,
fill_color="#3186cc",
popup=popup).add_to(marker_cluster)
map
First, we will define the styling of the London Boroughs layer. Folium includes the Branca library for choropleth color styling. We define the scale of the dataset and then apply a colormap. Branca has some built-in colormaps, but here we define our own. This color map was selected from the set of data-driven color schemes provided by CARTO. Lastly, we define a function to apply the color map, set opacity, and feature edge stroke.
# Choropleth styling
import branca.colormap as bcm
# Create color map
# "Fall" color ramp from https://carto.com/carto-colors/
scale = (boroughs['PlaqueDensity_sqkm'].quantile((0, 0.02, 0.5, 0.9, 0.98, 1))).tolist()
colormap = bcm.LinearColormap(colors=['#008080','#70a494', '#b4c8a8', '#edbb8a', '#de8a5a','#ca562c'],
index=scale,
vmin=min(boroughs['PlaqueDensity_sqkm']),
vmax=max(boroughs['PlaqueDensity_sqkm']))
style_function = lambda x: {
'fillColor': colormap(x['properties']['PlaqueDensity_sqkm']),
'color': 'black',
'weight': 1.5,
'fillOpacity': 0.3
}
Apply Boroughs features as a GeoJson overlay on the map with styling and a popup tooltip indicating the Borough name and number of Plaques.
Adding the colormap to the map adds a legend in the top right corner.
# Plot choropleth layer
folium.GeoJson(
boroughs,
style_function=style_function,
tooltip=folium.GeoJsonTooltip(
fields=['name', 'numPlaques'],
aliases=['Borough', 'Historic Markers'],
localize=True
)
).add_to(map)
colormap.caption = 'Historic markers per sq. km'
colormap.add_to(map)
That’s it! We have a great-looking map we can use to explore the dataset and learn about the history of London (and the rest of the UK too). You’ll notice that each of the Plaque popups contains a link to the OpenPlaques page with further information. Try looking around for yourself and see what interesting things you find.
One marker significant to the Geospatial community can be found at the corner of Broadwick Street and Lexington Street in the Borough of Westminster (southeast of Regent Street and Oxford Street).
The sign is dedicated to Dr. John Snow, an English physician who made significant advances in epidemiology and public health. Snow is recognized as one of the first to use geospatial analysis to understand disease in his work proving that Cholera was being spread through the London drinking water in the mid-1800s.
But I digress, the goal of this tutorial is to create an interactive map that you can share.
map
Folium has the functionality to export a map view to a Leaflet HTML file that can be rendered in a web browser. Let’s export our map as HTML and also save our processed Plaques and Boroughs data while we’re at it.
map.save("./index.html")
with open("./UK-plaques-2021-06-26.json", "w") as outfile:
outfile.write(plaques.to_json())
boroughs.to_file("./london-boroughs.geojson", driver="GeoJSON")
c:\Anaconda3\envs\gis_workbench\lib\site-packages\geopandas\io\file.py:362: FutureWarning: pandas.Int64Index is deprecated and will be removed from pandas in a future version. Use pandas.Index with the appropriate dtype instead. pd.Int64Index,
There are several ways to host web maps online in HTML format. GitHub Pages is a great option for smaller maps. However, the map we generated in this tutorial is too large to load on GitHub.
AWS S3 is a good option and offers a free tier of object storage up to 5GB. After creating an account, open the AWS s3 console, create a new bucket and apply the following settings:
In your new bucket, upload the HTML file you exported.
Lastly, you need to make the file public. Select the file in the AWS s3 console. Under “Actions” select “Make public using ACL”
Now click on the file and copy the object URL. This is a link that you can share with the public to access your web map hosted on s3.

This tutorial should have given you the experience you need to create engaging interactive web maps. Try this out on a dataset of interest to you and feel free to share the results with us on Twitter & LinkedIn.
Happy mapping :)